package org.gridkit.jvmtool;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.TimeZone;

import org.gridkit.jvmtool.cli.CommandLauncher;
import org.gridkit.jvmtool.cmd.AntPathMatcher;
import org.gridkit.jvmtool.event.ChainedEventReader;
import org.gridkit.jvmtool.event.ErrorHandler;
import org.gridkit.jvmtool.event.Event;
import org.gridkit.jvmtool.event.EventDumpParser;
import org.gridkit.jvmtool.event.EventMorpher;
import org.gridkit.jvmtool.event.EventReader;
import org.gridkit.jvmtool.event.ShieldedEventReader;
import org.gridkit.jvmtool.event.SimpleErrorEvent;
import org.gridkit.jvmtool.event.SingleEventReader;
import org.gridkit.jvmtool.event.TimestampedEvent;
import org.gridkit.jvmtool.stacktrace.ThreadEventCodec;
import org.gridkit.jvmtool.stacktrace.analytics.TimeRangeChecker;

import com.beust.jcommander.Parameter;

public abstract class AbstractEventDumpSource {

    protected CommandLauncher host;

    @Parameter(names={"-tr", "--time-range"}, required = false, description="Time range filter")
    protected String timeRange = null;

    @Parameter(names={"--parsers-info"}, required = false, description="Print parsers available in classpath")
    protected boolean reportParsers = false;
    
    private boolean wildcardFileMatching = true;
    
    protected TimeZone timeZone;
    
    private List<String> matchedInputFiles;
    
    public AbstractEventDumpSource(CommandLauncher host) {
        this.host = host;
    }
    
    public void setTimeZone(TimeZone tz) {
        this.timeZone = tz;
    }
    
    public void useWildcards(boolean enabled) {
    	wildcardFileMatching = enabled;
    }
    

    protected abstract List<String> inputFiles();
    
    public List<String> sourceFiles() {
    	if (matchedInputFiles == null) {

    		matchedInputFiles = new ArrayList<String>();
    		
        	if (wildcardFileMatching) {
    	    	List<String> rawInput = inputFiles();
    	
    		    AntPathMatcher matcher = new AntPathMatcher();
    		    matcher.setPathSeparator("/");
    		    
    			
    			for(String f: rawInput) {
    				f = f.replace('\\', '/');
    		        for(File ff: matcher.findFiles(new File("."), f)) {
    		            if (ff.isFile()) {
    		                matchedInputFiles.add(ff.getPath());
    		            }
    		        }
    			}
        	}
        	else {
        		matchedInputFiles.addAll(inputFiles());
        	}

    		if (matchedInputFiles.isEmpty()) {
                host.fail("No input files provided");
            }    		
    	}
    	return matchedInputFiles;
    }
    
    public EventReader<Event> getFilteredRawReader() {
    	if (timeRange != null) {
	        String[] lh = timeRange.split("[-]");
	        if (lh.length != 2) {
	            host.fail("Invalid time range '" + timeRange + "'", "Valid format yyyy.MM.dd_HH:mm:ss-yyyy.MM.dd_HH:mm:ss hours and higher parts can be ommited");
	        }
	        TimeRangeChecker checker = new TimeRangeChecker(lh[0], lh[1], timeZone);
	    	return getRawReader().morph(new TimeFilter(checker));
    	}
    	else {
    		return getRawReader();
    	}
    }
    
    public EventReader<Event> getRawReader() {

        final Iterator<String> it = sourceFiles().iterator();
        ChainedEventReader<Event> reader = new ChainedEventReader<Event>() {

            @Override
            protected EventReader<Event> produceNext() {
                return it.hasNext() ? openReader(it.next()) : null;
            }
        };

        ShieldedEventReader<Event> shieldedReader = new ShieldedEventReader<Event>(reader, Event.class, new ErrorHandler() {
            @Override
            public boolean onException(Exception e) {
                System.err.println("Stream reader error: " + e);
                return true;
            }
        });


        return shieldedReader;
    }    
    
    protected EventReader<Event> openReader(String file) {
    	if (reportParsers) {
    		printParsers();
    	}
        try {
            return ThreadEventCodec.createEventReader(new FileInputStream(file));
        } catch (IOException e) {
        	EventReader<Event> reader = openGenericDump(file);
        	if (reader != null) {
        		return reader;
        	}
        	else {
        		return new SingleEventReader<Event>(new SimpleErrorEvent(e));
        	}
        }
    }
    
    private void printParsers() {
    	System.out.println("Available file parsers");
    	for(String parser: ThreadEventCodec.listSupportedFormats()) {
    		System.out.println(" - " + parser);
    	}
    	ServiceLoader<EventDumpParser> loader = ServiceLoader.load(EventDumpParser.class);
		for(EventDumpParser edp: loader) {
			System.out.println(" - " + edp);
		}
	}

	protected EventReader<Event> openGenericDump(String file) {
		ServiceLoader<EventDumpParser> loader = ServiceLoader.load(EventDumpParser.class);
		FileSource fs = new FileSource(file);
		for(EventDumpParser edp: loader) {
			try {
				EventReader<Event> r = edp.open(fs);
				if (r != null) {
					return r;
				}
			}
			catch(Exception e) {
				// ignore
			}
			catch(NoClassDefFoundError ee) {
				ee.printStackTrace();
				// ignore
			}
		}
		return null;
    }
    
    static class TimeFilter implements EventMorpher<Event, Event> {
        
        TimeRangeChecker checker;
        
        public TimeFilter(TimeRangeChecker checker) {
            this.checker = checker;
        }

        @Override
        public Event morph(Event event) {
        	if (event instanceof TimestampedEvent) {
        		return checker.evaluate(((TimestampedEvent)event).timestamp()) ? event : null;
        	}
        	else {
        		return null;
        	}
        }
    }
    
    private static class FileSource implements EventDumpParser.InputStreamSource {

    	final String file;
    	InputStream is;
    	
		public FileSource(String file) {
			this.file = file;
		}

		@Override
		public InputStream open() throws IOException {
			if (is != null) {
				try {
					is.close();
				} catch (Exception e) {
					// ignore
				}
			}
			return is = new FileInputStream(file);
		}
    }
}
